using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Surface;
using Microsoft.Surface.Core;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;

namespace SurfaceRacing
{
    /// <summary>
    /// This is the main type for your application.
    /// </summary>
    public class App1 : Microsoft.Xna.Framework.Game
    {
        private readonly GraphicsDeviceManager graphics;
        private ContactTarget contactTarget;
        private Color backgroundColor = new Color(0, 0, 0);
        private bool applicationLoadCompleteSignalled;

        private UserOrientation currentOrientation = UserOrientation.Bottom;
        private Matrix screenTransform = Matrix.Identity;
        private Matrix inverted;

        // application state: Activated, Previewed, Deactivated,
        // start in Activated state
        private bool isApplicationActivated = true;
        private bool isApplicationPreviewed;

        #region variables

        SpriteFont courierNew;

        private SpriteBatch raceSprite;
        private Texture2D track;
        private Texture2D car;
        private Texture2D tagOrgiPosTexture;

        private float sizeOfCarHitbox;

        private float pedalDistance = 0f;

        private bool realisticSteering = false;

        private bool crashed = false;
        private float surfaceWidth;
        private float surfaceHeight;
        Random rand = new Random();

        Color[,] trackColors;

        private List<Car> cars = new List<Car>();

        private List<ByteTag> oldContacts = new List<ByteTag>();
        private Dictionary<ByteTag, float> firstTagRot = new Dictionary<ByteTag, float>();
        private Dictionary<ByteTag, Car> tagCarRelation = new Dictionary<ByteTag, Car>();

        private float maxPedalLenght = 100f;
        

        #endregion

        /// <summary>
        /// The graphics device manager for the application.
        /// </summary>
        protected GraphicsDeviceManager Graphics
        {
            get { return graphics; }
        }

        /// <summary>
        /// The target receiving all surface input for the application.
        /// </summary>
        protected ContactTarget ContactTarget
        {
            get { return contactTarget; }
        }

        /// <summary>
        /// Default constructor.
        /// </summary>
        public App1()
        {
            graphics = new GraphicsDeviceManager(this);
        }

        #region helper functions


        private Color[,] TextureTo2DArray(Texture2D texture)
        {
            Color[] colors1D = new Color[texture.Width * texture.Height];
            texture.GetData(colors1D);

            Color[,] colors2D = new Color[texture.Width, texture.Height];
            for (int x = 0; x < texture.Width; x++)
                for (int y = 0; y < texture.Height; y++)
                    colors2D[x, y] = colors1D[x + y * texture.Width];

            return colors2D;
        }

        private void carAtEdge(Car edgeCar)
        {
            
            if (edgeCar.Position.X >= surfaceWidth){
                edgeCar.Position = new Vector2(surfaceWidth-1, edgeCar.Position.Y);
            }
            if(edgeCar.Position.X <= 0){
                edgeCar.Position = new Vector2(0, edgeCar.Position.Y);

            }
            if(edgeCar.Position.Y >= surfaceHeight){
                edgeCar.Position = new Vector2(edgeCar.Position.X, surfaceHeight-1);
            }
            if (edgeCar.Position.Y <= 0)
            {
                edgeCar.Position = new Vector2(edgeCar.Position.X, 0);
            }
                        
        }


        #endregion


        #region Initialization

        /// <summary>
        /// Moves and sizes the window to cover the input surface.
        /// </summary>
        private void SetWindowOnSurface()
        {
            System.Diagnostics.Debug.Assert(Window.Handle != System.IntPtr.Zero,
                "Window initialization must be complete before SetWindowOnSurface is called");
            if (Window.Handle == System.IntPtr.Zero)
                return;

            // We don't want to run in full-screen mode because we need
            // overlapped windows, so instead run in windowed mode
            // and resize to take up the whole surface with no border.

            // Make sure the graphics device has the correct back buffer size.
            InteractiveSurface interactiveSurface = InteractiveSurface.DefaultInteractiveSurface;
            if (interactiveSurface != null)
            {
                graphics.PreferredBackBufferWidth = interactiveSurface.Width;
                graphics.PreferredBackBufferHeight = interactiveSurface.Height;
                graphics.ApplyChanges();

                // Remove the border and position the window.
                Program.RemoveBorder(Window.Handle);
                Program.PositionWindow(Window);
            }
        }

        /// <summary>
        /// Initializes the surface input system. This should be called after any window
        /// initialization is done, and should only be called once.
        /// </summary>
        private void InitializeSurfaceInput()
        {
            System.Diagnostics.Debug.Assert(Window.Handle != System.IntPtr.Zero,
                "Window initialization must be complete before InitializeSurfaceInput is called");
            if (Window.Handle == System.IntPtr.Zero)
                return;
            System.Diagnostics.Debug.Assert(contactTarget == null,
                "Surface input already initialized");
            if (contactTarget != null)
                return;

            // Create a target for surface input.
            contactTarget = new ContactTarget(Window.Handle, EventThreadChoice.OnBackgroundThread);
            contactTarget.EnableInput();
        }

        /// <summary>
        /// Reset the application's orientation and transform based on the current launcher orientation.
        /// </summary>
        private void ResetOrientation()
        {
            UserOrientation newOrientation = ApplicationLauncher.Orientation;

            if (newOrientation == currentOrientation) { return; }

            currentOrientation = newOrientation;

            if (currentOrientation == UserOrientation.Top)
            {
                screenTransform = inverted;
            }
            else
            {
                screenTransform = Matrix.Identity;
            }
        }

        #endregion

        #region Overridden Game Methods

        /// <summary>
        /// Allows the app to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            SetWindowOnSurface();
            InitializeSurfaceInput();

            // Set the application's orientation based on the current launcher orientation
            currentOrientation = ApplicationLauncher.Orientation;

            // Subscribe to surface application activation events
            ApplicationLauncher.ApplicationActivated += OnApplicationActivated;
            ApplicationLauncher.ApplicationPreviewed += OnApplicationPreviewed;
            ApplicationLauncher.ApplicationDeactivated += OnApplicationDeactivated;

            // Setup the UI to transform if the UI is rotated.
            // Create a rotation matrix to orient the screen so it is viewed correctly
            // when the user orientation is 180 degress different.
            inverted = Matrix.CreateRotationZ(MathHelper.ToRadians(180)) *
                       Matrix.CreateTranslation(graphics.GraphicsDevice.Viewport.Width,
                                                 graphics.GraphicsDevice.Viewport.Height,
                                                 0);

            if (currentOrientation == UserOrientation.Top)
            {
                screenTransform = inverted;
            }

            base.Initialize();
        }

        /// <summary>
        /// Load your graphics content.
        /// </summary>
        protected override void LoadContent()
        {
            surfaceHeight = Window.ClientBounds.Height;
            surfaceWidth = Window.ClientBounds.Width;
            raceSprite = new SpriteBatch(GraphicsDevice);
            car = Texture2D.FromFile(graphics.GraphicsDevice, @"Resources\Car.png");
            track = Texture2D.FromFile(graphics.GraphicsDevice, @"Resources\racetrack.png");
            //tagOrgiPosTexture = Texture2D.FromFile(graphics.GraphicsDevice, @"Resources\TagOrgiPosTexture.png");
            tagOrgiPosTexture = Texture2D.FromFile(graphics.GraphicsDevice, @"Resources\TagOrgiPosWControl.png");

            trackColors = TextureTo2DArray(track);

            //courierNew = Content.Load<SpriteFont>(@"Resources\SpriteFont1");
            courierNew = Content.Load<SpriteFont>(@"Resources\SpriteFont1");

        }

        /// <summary>
        /// Unload your graphics content.
        /// </summary>
        protected override void UnloadContent()
        {
            Content.Unload();
        }

        /// <summary>
        /// Allows the app to run logic such as updating the world,
        /// checking for collisions, gathering input and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (isApplicationActivated || isApplicationPreviewed)
            {
                if (isApplicationActivated)
                {
                    ReadOnlyContactCollection contacts = contactTarget.GetState();
                    foreach (Contact cont in contacts)
                    {
                        if (cont.IsTagRecognized)
                        {
                            
                            if (oldContacts.Contains(cont.Tag.Byte))
                            {
                                Car tmpCar = tagCarRelation[cont.Tag.Byte];
                                tmpCar.accelerate(tmpCar.TagPos.X - cont.CenterX);
                                pedalDistance = tmpCar.TagPos.X - cont.CenterX;
                                if (realisticSteering)
                                {
                                    if (Math.Abs(cont.Orientation - firstTagRot[cont.Tag.Byte]) < 2)
                                        tmpCar.WheelState = (cont.Orientation - firstTagRot[cont.Tag.Byte]) / 6;
                                    else
                                        tmpCar.WheelState = (cont.Orientation - firstTagRot[cont.Tag.Byte] - (float)Math.PI * 2) / 6;
                                    if (tmpCar.WheelState > 0.1)
                                        tmpCar.WheelState = 0.1f;
                                    else if (tmpCar.WheelState < -0.1)
                                        tmpCar.WheelState = -0.1f;
                                }
                                else
                                    tmpCar.Rotation = cont.Orientation;
                                tmpCar.HasUpdatedThisTurn = true;
                            }
                            else
                            {
                                Car tempCar = new Car(new Vector2(cont.CenterX, cont.CenterY));
                                cars.Add(tempCar);
                                oldContacts.Add(cont.Tag.Byte);
                                tagCarRelation.Add(cont.Tag.Byte, tempCar);
                                firstTagRot.Add(cont.Tag.Byte, cont.Orientation);
                            }
                        }
                        else if (cont.IsFingerRecognized)
                        {
                            Vector2 tmpPos = new Vector2(cont.CenterX, cont.CenterY);
                            foreach (Car car in cars)
                            {
                                if (Vector2.Distance(tmpPos, car.TagPos) < 15)
                                {
                                    car.TagPos = tmpPos;
                                    break;
                                }
                            }
                        }
                    }
                    

                    foreach (Car updateCar in cars)
                    {
                        carAtEdge(updateCar);
                        
                        
                        if (!updateCar.HasUpdatedThisTurn)
                        {
                            updateCar.accelerate(0f);
                            updateCar.HasUpdatedThisTurn = true;
                        }
                        if (realisticSteering)
                        {
                            if (updateCar.WheelState > Math.PI *2)
                                updateCar.WheelState -= (float)Math.PI *2;
                            else if (updateCar.WheelState < -Math.PI *2)
                                updateCar.WheelState += (float)Math.PI *2;
                            updateCar.Rotation += updateCar.WheelState * updateCar.Speed;
                        }

                        if (trackColors[(int)updateCar.Position.X, (int)updateCar.Position.Y] == Color.Black)
                        {
                            updateCar.move(1);
                        }
                        else
                        {
                            updateCar.move(2);
                        }
                        updateCar.HasUpdatedThisTurn = false;
                        
                    }

                }

                // TODO: Add your update logic here
            }
            else
            {
                Exit();
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the app should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            if (!applicationLoadCompleteSignalled)
            {
                // Dismiss the loading screen now that we are starting to draw
                ApplicationLauncher.SignalApplicationLoadComplete();
                applicationLoadCompleteSignalled = true;
            }

            //TODO: Rotate the UI based on the value of screenTransform here if desired

            graphics.GraphicsDevice.Clear(backgroundColor);

            raceSprite.Begin();

            //draw track
            raceSprite.Draw(track, new Vector2(surfaceWidth / 2, surfaceHeight / 2), null, Color.White, 0f, new Vector2(track.Width / 2, track.Height / 2), 1f, SpriteEffects.None, 1f);


            String text;
            foreach (Car drawCar in cars)
            {
                text = "Car" + (cars.IndexOf(drawCar)+1) + ": " + /*pedalDistance + "/" + drawCar.Speed + "\n" + */ drawCar.InertiaDirection.Length();
                raceSprite.Draw(car, drawCar.Position, null, drawCar.Color, drawCar.Rotation, new Vector2(car.Width / 2, car.Height / 1f), 1f, SpriteEffects.None, 1f);
                raceSprite.DrawString(courierNew, text, new Vector2(200, 200 + 50 * cars.IndexOf(drawCar)), Color.White);
                raceSprite.Draw(tagOrgiPosTexture, drawCar.TagPos, null, drawCar.Color, 0f, new Vector2(tagOrgiPosTexture.Width / 2, tagOrgiPosTexture.Height / 2), 1f, SpriteEffects.None, 1f);
            
            }

            raceSprite.End();

            
            base.Draw(gameTime);
        }

        #endregion

        #region Application Event Handlers

        /// <summary>
        /// This is called when application has been activated.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnApplicationActivated(object sender, EventArgs e)
        {
            // Update application state.
            isApplicationActivated = true;
            isApplicationPreviewed = false;

            // Orientaton can change between activations.
            ResetOrientation();

            //TODO: Enable audio, animations here

            //TODO: Optionally enable raw image here
        }

        /// <summary>
        /// This is called when application is in preview mode.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnApplicationPreviewed(object sender, EventArgs e)
        {
            // Update application state.
            isApplicationActivated = false;
            isApplicationPreviewed = true;

            //TODO: Disable audio here if it is enabled

            //TODO: Optionally enable animations here
        }

        /// <summary>
        ///  This is called when application has been deactivated.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnApplicationDeactivated(object sender, EventArgs e)
        {
            // Update application state.
            isApplicationActivated = false;
            isApplicationPreviewed = false;

            //TODO: Disable audio, animations here

            //TODO: Disable raw image if it's enabled
        }

        #endregion

        #region IDisposable

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Release managed resources.
                IDisposable graphicsDispose = graphics as IDisposable;
                if (graphicsDispose != null)
                {
                    graphicsDispose.Dispose();
                }
                if (contactTarget != null)
                {
                    contactTarget.Dispose();
                    contactTarget = null;
                }
            }

            // Release unmanaged Resources.

            // Set large objects to null to facilitate garbage collection.

            base.Dispose(disposing);
        }


        #endregion

    }
}
